Skip to content

fix: port upstream #42 fixes (EIS error contract, at-spi launcher, screenshot D-Bus-first) - #10

Merged
VibeProgramm merged 4 commits into
mainfrom
fix/port-upstream-42-ecf
Sep 6, 2026
Merged

fix: port upstream #42 fixes (EIS error contract, at-spi launcher, screenshot D-Bus-first)#10
VibeProgramm merged 4 commits into
mainfrom
fix/port-upstream-42-ecf

Conversation

@VibeProgramm

@VibeProgramm VibeProgramm commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Точечный порт трёх фиксов из upstream-PR isac322#42 (не вмержен в апстрим, апстрим мержит редко — решение форка: порт, не merge): контракт ошибок EIS, резолвер at-spi-bus-launcher, ScreenShot2-first для capture_screenshot_to_file. Три атомарных коммита fix(input) / fix(session) / fix(screenshot) + chore(release): v0.8.1 (версия, CHANGELOG, манифесты плагинов через sync_plugin_version.py).

Motivation

Контракт на результат — issue #8. Детали по каждому фиксу ниже.

1. EIS error contract — fix(input)

EISClient._setup (input.py): dbus-часть get_object → Interface → connectToEIS обёрнута в try/except dbus.DBusExceptionRuntimeError("KWin EIS interface unavailable: {exc}") с цепочкой from exc. Раньше любое D-Bus-падение роняло весь session_start/session_connect: dbus.DBusException — не RuntimeError, а core.py ловит только RuntimeError для деградации до "no input backend". Больше ничего не тронуто: ToolError-логика _negotiate_devices (форковая, строже апстрима) не затронута.

2. Резолвер at-spi-bus-launcher — fix(session)

В bash-обёртке был захардкожен архловский путь /usr/lib/at-spi-bus-launcher; на Debian/Ubuntu/Fedora бинарник в /usr/libexec/at-spi-bus-launcher → тихий no-op, мёртвая accessibility-шина. Новый модульный резолвер _at_spi_bus_launcher(): кандидаты /usr/libexec/at-spi-bus-launcher, /usr/lib/at-spi-bus-launcher, /usr/lib/at-spi2-core/at-spi-bus-launcher (первый существующий по Path(...).exists()), затем shutil.which, затем дефолт-кандидат Arch. Резолв происходит на Python-стороне до генерации обёртки. На Arch (единственная платформа форка на практике) путь не меняется. Env-логика _build_env (KDE_FULL_SESSION и пр.) не тронута.

3. ScreenShot2-first для screenshot — fix(screenshot)

capture_screenshot_to_file безусловно звал spectacle, вопреки своей документации; в минимальных/виртуальных сессиях spectacle может отсутствовать. Теперь: сначала ScreenShot2 D-Bus (адаптация логики _capture_frame_burst_dbus для одиночного кадра), при dbus.DBusException/RuntimeError — fallback _capture_via_spectacle, при двойном падении — RuntimeError с обеими ошибками. Контракт возвращаемого значения (Path, как зовёт core.py) не изменён.

Пиксельный pipe теперь дренируется конкурентно (поток-читатель, os.read-цикл, fd закрыт в finally читателя — KWin стримит пиксели до D-Bus-ответа), dbus-вызов с timeout=5.0 вместо 25-секундного дефолта dbus-python.

Отклонение от апстрима (осознанное): empty-check кадра вынесен из хелпера _capture_raw_frame в capture_screenshot_dbus — frame-burst исторически скипает пустые кадры (phase 2), и апстримный вариант (RuntimeError на пустой кадр внутри хелпера) ронял бы весь burst. Поведение frame-burst не изменилось.

Чего не делал (вне объёма, issue #8)

Остальные фиксы isac322#42: (a) KDE env — у форка осознанно инвертировано; (b) bounded handshake — уже есть сильнее (adopted из isac322#50); (d) GI enum — не срочно; (g) EI_EVENT_DEVICE_RESUMED — уже есть, строже апстрима. Docker e2e — отдельная отложенная issue #9.

How to Test

Локальный прогон (соответствует CI):

  1. uv run ruff check . — pass (baseline на main: тоже pass)
  2. uv run ruff format --check . — pass
  3. uv run ty check — pass
  4. uv run pytest tests/ -q65 passed (baseline: 56 passed; +9 новых тестов). Тесты, требующие живого KWin, в наборе отсутствуют — скипнутых нет ни до, ни после.

Живой прогон не выполнялся агентом (нужен живой KDE Wayland-десктоп). Человеку стоит проверить:

  • session_connect к живому десктопу → при недоступном EIS-интерфейсе сессия стартует со строкой "No input backend..." вместо падения;
  • session_start (виртуальная сессия) → AT-SPI-дерево непустое (путь резолвера на Arch должен остаться /usr/lib/at-spi-bus-launcher);
  • screenshot в виртуальной сессии → кадр снимается через ScreenShot2 (быстро), при недоступности — fallback spectacle.

Checklist

  • uv run ruff check . passes
  • uv run ruff format --check . passes
  • uv run ty check passes
  • CHANGELOG.md updated (if user-facing change)
  • README.md updated (if new tools or changed behavior)

Closes

Closes #8

…meError

Unprotected get_object/Interface/connectToEIS calls crashed the whole
session_start/session_connect: dbus.DBusException is not a RuntimeError,
and core.py catches only RuntimeError to degrade to "no input backend".
Wrap the dbus block and re-raise as
RuntimeError("KWin EIS interface unavailable: {exc}") with error chaining
(adopted from upstream isac322#42).
…oded Arch path

The bash wrapper hardcoded /usr/lib/at-spi-bus-launcher (Arch layout). On
Debian/Ubuntu/Fedora the binary lives in /usr/libexec (or
/usr/lib/at-spi2-core), so the wrapper line silently no-oped and the
session's accessibility bus was dead. Resolve on the Python side before
assembling the wrapper: first existing candidate from
(/usr/libexec, /usr/lib, /usr/lib/at-spi2-core), then shutil.which, then
the Arch default (adopted from upstream isac322#42).
…_file

capture_screenshot_to_file unconditionally invoked the spectacle CLI,
contrary to its own documentation, and minimal/virtual sessions may not
have spectacle installed at all. Try the ScreenShot2 D-Bus capture first
and fall back to spectacle on DBusException/RuntimeError; when both fail,
raise a RuntimeError carrying both causes (adopted from upstream
isac322#42).

The shared single-frame helper _capture_raw_frame drains the pixel pipe
concurrently with the D-Bus call (KWin streams pixels before replying), a
reader thread owns read_fd and closes it in its finally block, and the
call carries a 5s timeout instead of dbus-python's 25s default. The frame
burst path now goes through the same helper; empty frames keep being
skipped there instead of aborting the burst.
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

📝 Docs & SEO Review

Source files changed in this PR:

.claude-plugin/marketplace.json
integrations/claude-code/.claude-plugin/plugin.json
integrations/opencode/plugin/package.json
pyproject.toml
src/kwin_mcp/input.py
src/kwin_mcp/screenshot.py
src/kwin_mcp/session.py

Consistency check results:

✅  All documentation/plugin SEO checks passed.

Run @docs-seo in Claude Code to perform a full documentation review.

@VibeProgramm
VibeProgramm merged commit 0cb3870 into main Sep 6, 2026
9 checks passed
@VibeProgramm
VibeProgramm deleted the fix/port-upstream-42-ecf branch September 6, 2026 23:13
VibeProgramm added a commit that referenced this pull request Sep 6, 2026
…end degradation logging, test cleanups (#11)

* fix(session): Arch default as last-resort at-spi launcher fallback, quote it in the wrapper

The final fallback of _at_spi_bus_launcher returned candidates[0]
(/usr/libexec, the Debian/Ubuntu/Fedora layout), which does not exist on
Arch — a session where neither a candidate file nor PATH lookup finds the
launcher embedded a dead path into the wrapper. The last resort is now
the Arch default /usr/lib/at-spi-bus-launcher (literal, not a candidate
index, so reordering candidates cannot repoint it).

The resolved path is shlex.quote'd when embedded into the bash wrapper;
shlex.quote leaves plain paths untouched, so behavior is unchanged on
typical distros.

Test updated to expect the Arch default; the wrapper test now also
covers a path that requires quoting.

* fix(core): log the input-backend degradation reason instead of swallowing it

Both InputBackend failure sites (session_start, session_connect) caught
RuntimeError and silently degraded to 'no input backend' / ydotool, so
the actual reason (EIS unavailable, dbus failure, libei load error) was
lost. Each site now logs a warning with the exception text before
degrading; backend selection itself is unchanged.

kwin_mcp has no logging module anywhere else; logging-to-stderr is the
standard channel for stdio MCP servers whose stdout carries the
protocol.

* chore(tests): remove dead assignment, tautological assert, add type hints

- test_input_eis_error: dropped a dead _client_with_bus(object())
  assignment immediately overwritten by _client_with_bus(_OkBus());
  no side effects (pure constructor stub).
- test_screenshot_fallback: 'assert Image' was always true; replaced
  with a real assertion that phase 2 never calls Image.frombytes for
  the skipped empty frame.
- Type hints on fakes/helpers in both files (monkeypatch -> pytest.
  MonkeyPatch, tmp_path -> Path, fake signatures), per CONTRIBUTING
  style rules. Pre-existing tests outside the PR #10 diff untouched.

* chore(release): v0.8.2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Портировать из upstream #42: контракт ошибок EIS, резолвер at-spi-bus-launcher, ScreenShot2-first для screenshot

1 participant